package com.nulldev.util.net.tcp.server;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.PrivateKey;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Pattern;

import com.nulldev.util.FinalBypass;
import com.nulldev.util.crypto.api2.EncryptionUtil;
import com.nulldev.util.crypto.encryptAPI.rsa.RSA;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.java.DataSet;
import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.java.Variables;
import com.nulldev.util.lang.json.JsonObject;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.napi.nullAPI;
import com.nulldev.util.net.IPUtil;
import com.nulldev.util.net.dsap.Server;
import com.nulldev.util.net.packets.ConnectionManager;
import com.nulldev.util.net.packets.NetInfo;
import com.nulldev.util.net.packets.NetPacket;
import com.nulldev.util.net.packets.PacketManager;
import com.nulldev.util.net.packets.PacketMode;
import com.nulldev.util.net.sdtp.SDTPType;
import com.nulldev.util.net.tcp.TCPCore;
import com.nulldev.util.net.tcp.fRetry;
import com.nulldev.util.net.tcp.client.TCPClient;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.other.ByteUtils;
import com.nulldev.util.other.CharsetUtil;
import com.nulldev.util.threading.legacy.ThreadExecutor;

abstract public class TCPServer extends fRetry implements Server {
	protected final Logger logger = LoggerFactory.getLoggerD(
			JVMUtil.isArg("--network.logging"), true, false);
	final CopyOnWriteArrayList<TCPCore> connections = new CopyOnWriteArrayList();
	private final int port;
	private final boolean localIP;
	private InetAddress ip;
	private ServerSocket server;
	
	private final DataSet<byte[], byte[]> _ncrypt_keys;

	public TCPServer (String category, String name, int port) {
		super(category, name);
		this.port = port;
		this.localIP = true;
		this._ncrypt_keys = RSA.generateKey();
	}
	
	public TCPServer (String category, String name, int port, final boolean localIP) {
		super(category, name);
		this.port = port;
		this.localIP = localIP;
		this._ncrypt_keys = RSA.generateKey();
	}
	
	public byte[] getPublicKey() {
		return this._ncrypt_keys.getA();
	}
	
	public String getPublicKeyAsBase64() {
		return ByteUtils.toBase64(this.getPublicKey());
	}
	
	public byte[] getPrivateKey() {
		return this._ncrypt_keys.getB();
	}
	
	public boolean isLocal() {
		return this.localIP;
	}

	public int getPort() {
		return this.port;
	}
	
	public String getIP() {
		if (this.localIP) {
			return "127.0.0.1";
		}
		try {
			return this.ip.getHostAddress();
		}catch (Exception e) {
			try {
				return IPUtil.getLocalHostLANAddress().getHostAddress();
			}catch (Exception e1) {
				return "127.0.0.1";
			}
		}
	}
	
	private Socket socket;
	protected void retry() {
		try {
			if (this.localIP) {
				server = new ServerSocket();
				server.bind(new InetSocketAddress("127.0.0.1", port));
			}else {
				server = new ServerSocket();
				server.bind(new InetSocketAddress(IPUtil.getLocalHostLANAddress(), port));
			}
		} catch (Exception ex) {
			if (logger.errorEnabled) logger.error(category, "Unable to start TCP server.", ex);
			failed();
			return;
		}
		try {
			if (this.localIP) {
				if (logger.infoEnabled) logger.info(category, "Listening on port: " + port);
			}else {
				if (logger.infoEnabled) logger.info(category, "Listening on ip: " + this.server.getInetAddress().getHostAddress() + ", port: " + port);
			}
			while (running) {
				socket = server.accept();
				socket.setKeepAlive(true);
				this.onSocketCreate(socket);
				success();
				try {
					ServerConnection connection = new ServerConnection(category, name, socket);
					connections.add(connection);
					this.onServerConnection(connection);
					if (logger.traceEnabled) logger.trace(category,
							"Client connected: " + socket.getInetAddress() + ":" + socket.getPort());
					connection.start();
					connected(connection);
				} catch (Exception ex) {
					if (logger.errorEnabled) logger.error(category, "Error configuring client connection.", ex);
				}
			}
		} catch (Exception ex) {
			if (logger.errorEnabled) logger.error(category, "Unexpected server error.", ex);
			IOUtils.closeQuietly(server);
			failed();
		}
	}

	protected void stopped () {
		IOUtils.closeQuietly(server);
	}

	public void connected (TCPCore connection) {
	}

	public void disconnected (TCPCore connection) {
	}

	public List<TCPCore> getConnections () {
		return connections;
	}
	
	public CopyOnWriteArrayList<ServerConnection> getConnectionsClone() {
		return (CopyOnWriteArrayList<ServerConnection>) connections.clone();
	}
	
	public Socket getSocket() {
		return this.socket;
	}

	public void send(String message) {
		for (TCPCore connection : connections)
			connection.send(message);
	}

	public boolean sendBlocking(String message) {
		boolean success = true;
		for (TCPCore connection : connections)
			if (!connection.sendBlocking(message)) success = false;
		return success;
	}

	public boolean sendBlocking(String message, byte[] bytes) {
		return sendBlocking(message, bytes, 0, bytes.length);
	}

	public boolean sendBlocking(String message, byte[] bytes, int offset, int count) {
		boolean success = true;
		for (TCPCore connection : connections)
			if (!connection.sendBlocking(message, bytes, offset, count)) success = false;
		return success;
	}

	abstract public void receive(TCPCore connection, String event, String payload, byte[] bytes, long length, final long readTime);

	public void close () {
		for (TCPCore connection : connections) {
			connection.close();
			disconnected(connection);
		}
		connections.clear();
	}
	
	public Logger getLogger() {
		return this.logger;
	}
	

	public class ServerConnection extends TCPCore implements Server {
		
		private final nullAPI apiCalls = ConnectionManager.getServerConnection(this);
		
		public ServerConnection (String category, String name, Socket socket) throws IOException {
			super(category, name, socket);
		}

		public boolean isValid() {
			if (connections.contains(this)) return true;
			return false;
		}

		public void receive (String event, String payload, byte[] bytes, long count, final long readTime) {
			TCPServer.this.receive(this, event, payload, bytes, count, readTime);
		}

		public boolean close () {
			super.close();
			connections.remove(this);
			return true;
		}
		
		private String key = "";
		public String getKey() {
			return this.key;
		}
		
		public void setKey(final String key) {
			this.key = key;
		}

		public String getIP() {
			if (Variables.notNull(TCPServer.this.ip)) {
				return TCPServer.this.ip.getHostAddress();
			}else {
				return "127.0.0.1";
			}
		}
		
		public int getPort() {
			return TCPServer.this.port;
		}
		
		public CopyOnWriteArrayList<ServerConnection> getClients() {
			return (CopyOnWriteArrayList<ServerConnection>) TCPServer.this.connections.clone();
		}
		
		public <T> Object invoke(final String command, final T object) {
			return this.apiCalls.invoke(command, object);
		}
		
		public <T> Object invoke(final String command) {
			return this.apiCalls.invoke(command);
		}

		public <T> Object invoke(final String command, final Object... object) {
			return this.apiCalls.invoke(command, object);
		}
		
		public boolean isSDTPSupported() {
			return TCPServer.this.isSDTPSupported();
		}

		public TCPServer getTCPServer() {
			return TCPServer.this;
		}

		private byte[] publicKey;
		public void setPublicKey(final byte[] keyData) {
			if (keyData != null) {
				this.publicKey = keyData;
			}
		}	
		
		public boolean hasPublicKey() {
			return this.publicKey != null;
		}
		
		public byte[] getPublicKey() {
			return this.publicKey;
		}
		
		public boolean annouceServerKeys() {
			return this.sendBlocking(PacketManager.fromID("npcrypt.public", TCPServer.this.getPublicKeyAsBase64()));
		}
		
		public String attemptEncryptionV3(final NetPacket packet) {
			if (packet == null) {
				throw new RuntimeException("packet == null!");
			}
			final boolean hasPublicKey = this.hasPublicKey();
			if (hasPublicKey) {
				final byte[] pubKey = this.getPublicKey();
				final String data = ByteUtils.toBase64(RSA.encrypt(RSA.getPublicKey(pubKey), packet.getData()));
				return "[NP3{" + packet.getID() + ",ENC_NCRYPT}]" + data;
			}
			return packet.toString();
		}

		@Override
		public boolean isDSAPready() {
			return TCPServer.this.isDSAPready();
		}

		@Override
		public boolean supportsSDTP() {
			return TCPServer.this.supportsSDTP();
		}

		@Override
		public boolean supportsDSAP_SecureStreams() {
			return TCPServer.this.supportsDSAP_SecureStreams();
		}

		@Override
		public void closeDSAP() {
			TCPServer.this.closeDSAP();
		}

		@Override
		public boolean isDSAPSpawn() {
			return TCPServer.this.isDSAPSpawn();
		}
		
		// MOVE BELOW TO nullAPI WRAPPER
		
//		public void createClient(final String name) {
//			this.client = new Client(name, this);
//		}
//
//		public Client getClient() {
//			return this.client;
//		}
//		
//		public CopyOnWriteArrayList<ServerConnection> getClientsNoSelf(final NetInfo client) {
//			final CopyOnWriteArrayList<ServerConnection> out = (CopyOnWriteArrayList<ServerConnection>) TCPServer.this.connections.clone();
//			out.remove(client.getServerConnection());
//			return out;
//		}
//		
//		public CopyOnWriteArrayList<ServerConnection> getClientsNoSelfChannel(final NetInfo client, String channel) {
//			final CopyOnWriteArrayList<ServerConnection> conns = getClientsNoSelf(client);
//			for (int i = 0; i < conns.size(); i++) {
//				final ServerConnection c = conns.get(i);
//				if (!c.getClient().getChannel().getChannel().equals(channel)) {
//					conns.remove(i);
//				}
//			}
//			return conns;
//		}
//
//		public void sendAll(final NetPacket all) {
//			for (final ServerConnection connection : this.getClients()) {
//				connection.sendBlocking(all);
//			}
//		}
//		
//		public void reply(final NetPacket packet) {
//			this.sendBlocking(packet);
//		}
//
//		public boolean isSDTPSupported() {
//			return TCPServer.this.isSDTPSupported();
//		}
//
//		public String[] getUsers() {
//			final ArrayList<String> out = new ArrayList<String>();
//			for (final ServerConnection conn : this.getClients()) {
//				if (conn.getClient() != null) {
//					out.add(conn.getClient().name());
//				}
//			}
//			if (JVMUtil.isArg("--testing.users.ui")) {
//				logger.warn("--testing.users.ui argument found. Creating load...");
//				for (int i = 0; i < 100000; i++) {
//					if (i % 10000 == 0) {
//						System.out.println("--testing.users.ui argument found. Creating load..." + i);
//					}
//					final String rnd = StringsUtil.generateSecure(6);
//					out.add(rnd);
//				}
//				logger.warn("--testing.users.ui: Done.");
//			}
//			return ArrayUtils.arrayListToArray(out);
//		}
//
//		public boolean isChannel(final Channel channel) {
//			return ArrayUtils.contains(TCPServer.this.CHANNELS.getObject(), channel);
//		}
//
//		public CopyOnWriteArrayList<ServerConnection> getClientsOnChannel(final String channel) {
//			final CopyOnWriteArrayList<ServerConnection> conns = getClients();
//			for (int i = 0; i < conns.size(); i++) {
//				final ServerConnection c = conns.get(i);
//				if (!c.getClient().getChannel().getChannel().equals(channel)) {
//					conns.remove(i);
//				}
//			}
//			return conns;
//		}
//
//		public Channel[] getChannels() {
//			return TCPServer.this.getChannels();
//		}
//		
//		public String[] getChannelsString() {
//			final ArrayList<String> ch = new ArrayList<>();
//			for (final Channel s : TCPServer.this.getChannels()) {
//				ch.add(s.getChannel());
//			}
//			return ArrayUtils.arrayListToArray(ch);
//		}
//		
//		public String[] getChannelsStringBF() {
//			final ArrayList<String> ch = new ArrayList<>();
//			for (final Channel s : TCPServer.this.getChannels()) {
//				ch.add(s.toString());
//			}
//			return ArrayUtils.arrayListToArray(ch);
//		}
//		
//		public Channel getChannelForString(String channel) {
//			return TCPServer.this.forName(channel);
//		}
//		
//		public boolean compareServePassword(final String input) {
//			return TCPServer.this.comparePassword(input);
//		}
//
//		public boolean serverHasPassword() {
//			return TCPServer.this.hasPassword();
//		}
//
//		public void setChannels(final Channel... channels) {
//			TCPServer.this.setChannels(channels);
//		}
//
//		public String getServerPassword() {
//			return TCPServer.this.serverPassword;
//		}

	}

	private boolean allowSDTP;
	public SDTPType[] SUPPORTED_TYPES = {SDTPType.IP_LOOKUP};
	
	public boolean isSupported(final SDTPType type) {
		return ArrayUtils.contains(this.SUPPORTED_TYPES, type);
	}
	
	public boolean isSDTPSupported() {
		return this.allowSDTP;
	}
	
	public void setSDTPSupported(final boolean sdtp) {
		this.allowSDTP = sdtp;
	}
	
	//FIXME
	
	private boolean allowDSAP = true, allowDSAPEncryption = true;
	
	@Override
	public boolean isDSAPready() {
		return this.allowDSAP;
	}

	@Override
	public boolean supportsSDTP() {
		return this.allowSDTP;
	}

	@Override
	public boolean supportsDSAP_SecureStreams() {
		return this.allowDSAPEncryption;
	}
	
	@Override
	public void closeDSAP() {
		if (!this.isDSAPSpawn()) {
			return;
		}
		LoggerFactory.getLoggerName("DSAP", true, true).info("Allocated server on " + this.getIP() + ":" + this.getPort() + " was closed.");
		this.close();
	}
	
	@Override
	public boolean isDSAPSpawn() {
		return false;
	}
	
	public void broadcast(final NetPacket packet) {
		ThreadExecutor.execute(new Runnable() {
			
			@Override
			public void run() {
				for (final ServerConnection sc : (CopyOnWriteArrayList<ServerConnection>) TCPServer.this.connections.clone()) {
					sc.send(packet);
				}
			}
		});
	}
	
	public void broadcastV3(final NetPacket packet) {
		ThreadExecutor.execute(new Runnable() {
			
			@Override
			public void run() {
				for (final ServerConnection sc : (CopyOnWriteArrayList<ServerConnection>) TCPServer.this.connections.clone()) {
					sc.send(packet);
				}
			}
		});
	}
	
	public void broadcastV3(final NetPacket packet, final PacketMode mode) {
		this.broadcastV3(packet, mode, null);
	}
	
	public void broadcastV3(final NetPacket packet, final PacketMode mode, final NetInfo info) {
		ThreadExecutor.execute(new Runnable() {
			
			@Override
			public void run() {
				for (final ServerConnection sc : (CopyOnWriteArrayList<ServerConnection>) TCPServer.this.connections.clone()) {
					sc.sendBlocking(packet, mode, info);
				}
			}
		});
	}
	
	public void broadcastV3ENC(final NetPacket packet) {
		ThreadExecutor.execute(new Runnable() {
			
			@Override
			public void run() {
				for (final ServerConnection sc : (CopyOnWriteArrayList<ServerConnection>) TCPServer.this.connections.clone()) {
					sc.send(sc.attemptEncryptionV3(packet));
				}
			}
		});
	}
	
	public void broadcastV3ENCF(final NetPacket packet, final NetInfo info) {
		ThreadExecutor.execute(new Runnable() {
			
			@Override
			public void run() {
				for (final ServerConnection sc : (CopyOnWriteArrayList<ServerConnection>) TCPServer.this.connections.clone()) {
					sc.sendBlocking(packet, PacketMode.STANDARD_V2_FULLY_ENCRYPTED, info);
				}
			}
		});
	}
	
	public void broadcastV3C(final NetPacket packet) {
		packet.preferBytePayload(true);
		ThreadExecutor.execute(new Runnable() {
			
			@Override
			public void run() {
				for (final ServerConnection sc : (CopyOnWriteArrayList<ServerConnection>) TCPServer.this.connections.clone()) {
					sc.sendBlocking(packet, PacketMode.STANDARD_V2_COMPRESSED);
				}
			}
		});
	}


	@Deprecated
	static public void main () throws Exception {
		TCPServer server = new TCPServer("server", "TestServer", 4569) {
			public void receive (TCPCore connection, String event, String payload, byte[] bytes, long length, final long readTime) {
				String eventF = EncryptionUtil.byID("p2pC").decrypt(event, "localhost:4569");
				eventF = eventF.replaceAll(Pattern.quote("%space"), " ");
				System.out.println("Incoming packet size '" + ByteUtils.calc(event.getBytes(CharsetUtil.getCharset()), CharsetUtil.getCharset()) + "'.");				System.out.println("Server received: " + eventF + ", " + payload);
				send("I got '" + event + "'.");
			}

			@Override
			public void closeDSAP() {
				this.close();
			}
		};
		server.start();

		final FinalBypass<Integer> packets = new FinalBypass<Integer>(1);
		TCPClient client = new TCPClient("client", "TestClient", "localhost", 4569) {
			public void receive (String event, String payload, byte[] bytes, long count, final long readTime) {
				System.out.println("Client received: " + event + ", " + payload);
				getConnection().close();
				packets.add(1);
				if (packets.getObject() >= 2) {
					JVMUtil.halt();
				}
			}

			@Override
			public void onDSAP(JsonObject obj) {
			}
		};
		client.start();
		client.waitForConnection(0);
		client.sendBlocking(EncryptionUtil.byID("p2pC").encrypt("__sctp2{0x0000, data{'null' ,'\u0028\u0361\u00b0\u035c\u0296\u0361\u00b0\u0029 | LMAO WHO DID THIS HAHAHAHAHA'}}", "localhost:4569"));
		client.sendBlocking(EncryptionUtil.byID("p2pC").encrypt("__sctp2{0x004E, data{null}}", "localhost:4569"));
	}

	public void onSocketCreate(final Socket socket) {
	}
	
	public void onServerConnection(final ServerConnection conn) {}

	public boolean hasPrivateKey() {
		return this.getPrivateKey() != null;
	}
	
	/**
	 * Decrypts data from NPCrypt
	 * @param enc_ncrypt
	 * @returns String
	 */
	public String decrypt(final String enc_ncrypt) {
		//JVMUtil.async_error("decrypt(String): hasPrivateKey=" + this.hasPrivateKey());
		if (!hasPrivateKey()) {
			return enc_ncrypt;
		}
		try {
			final PrivateKey pk = RSA.getPrivateKey(this.getPrivateKey());
			return new String(RSA.decrypt(pk, ByteUtils.fromBase64(enc_ncrypt)));
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	public byte[] decrypt(final byte[] enc_ncrypt) {
		//JVMUtil.async_error("decrypt(byte[]): hasPrivateKey=" + this.hasPrivateKey());
		if (!hasPrivateKey()) {
			return enc_ncrypt;
		}
		try {
			final PrivateKey pk = RSA.getPrivateKey(this.getPrivateKey());
			return RSA.decrypt(pk, enc_ncrypt);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

